#include <glad/glad.h>
#include <glm_no_warnings.h>
#include <stb_image_no_warnings.h>

//make sure you include glad before SDL!
#include "applogger.h"
#include "appdelegate.h"
#include "shaderprogram.h"
#include "resourcemanager.h"


#include "draw.h"


#include<iostream>
#include <vector>
#include <thread>//time management
#include <chrono>

AppDelegate::AppDelegate()
    :mRunning(true),mWidth(800),mHeight(600),
    mWorld(b2Vec2(0.0f, 0.0f)) //use gravity to initialize a physic mWorld
{

    if (SDL_Init(SDL_INIT_EVERYTHING) < 0) 
    {
        error("SDL INIT FAILED.");
        mRunning = false;
    }
    //Use OpenGL 3.3 core
	SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3 );
	SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 3 );
	SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE );

    // get a window 
    // SDL_CreateWindow(const char* title, int x, int y, int width, int height, Uint32 flags);
	mWindow = SDL_CreateWindow("Break Out", 
									SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 
									800, 600, 
									SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN );
    mContext = SDL_GL_CreateContext(mWindow);
    // glad: load all OpenGL function pointers
	if(!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress))
	{
         error("Failed to initialize GLAD");
         mRunning = false;
    }

    // //cull front face! note this!!!
    // glEnable(GL_CULL_FACE);//cull front face! note this!!!
    // //cull front face! note this!!!
    // glCullFace(GL_FRONT);
    // allow alpha blend
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // load image with opengl coordinate
    stbi_set_flip_vertically_on_load(true);

    bgm  = new MusicPlayer("resources/imgs/breakout.wav");

    bgm->play();
}

AppDelegate::~AppDelegate()
{
    // DO NOT FORGET !!! to delete whatever you new, 
    // however, b2Body-s are automaticly deleted by Box2D
    delete player;
    delete Ball;
    delete bgm;
    delete Renderer;
    for(auto &item : Levels)
    {
        delete item;
    }
    ResourceManager::Clear();
    SDL_GL_DeleteContext(mContext);
	SDL_DestroyWindow(mWindow);
    SDL_Quit();
}

int AppDelegate::OnExecute()
{
    OnInitData();

    g_debugDraw.Create();

    g_debugDraw.SetFlags(b2Draw::e_shapeBit|b2Draw::e_jointBit|b2Draw::e_aabbBit);
    mWorld.SetAllowSleeping(true);
    mWorld.SetWarmStarting(true);
    mWorld.SetContinuousPhysics(true);

    // DeltaTime variables
    std::chrono::duration<double> frameTime(0.0);
	std::chrono::duration<double> sleepAdjust(0.0);

    SDL_Event event;
    while(mRunning)
    {
        std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
        processInput(0);
        while(SDL_PollEvent(&event))
        {
            OnEvent(&event);
        }
        
        
         /** Simulation **/
        mWorld.Step(1.0f/120, 8, 2);

        auto speed = Ball->getPhysicBody()->GetLinearVelocity();
        speed.Normalize();
        Ball->getPhysicBody()->SetLinearVelocity(40*speed);

        /** Display **/
        OnRender();
        // ---debug draw----
        // uncomment these to show physics world
        // for debug use only!

        // mWorld.SetDebugDraw(&g_debugDraw);
        // mWorld.DebugDraw();
        // g_debugDraw.Flush();

        // Update particles
        Particles->Update(1, *Ball, 2);

        // Collision detect
        std::vector<b2Body*> dead;
        auto lists = Ball->getPhysicBody()->GetContactList();
        auto ball = Ball->getPhysicBody();
        for(auto list=lists;list;list = list->next)
        { 
            if(list->contact->IsTouching())
            {
                auto game_object = reinterpret_cast<GameObject *>(list->other->GetUserData());
                if(game_object && game_object->mTag == 1 && !game_object->isDestroyed())
                {
                    game_object->setAsDestroyed(true);
                    dead.push_back(game_object->getPhysicBody());
                    // do not destroy here, which may raise exception
                    // mWorld.DestroyBody(game_object->getPhysicBody());
                }
            }
           
        }
        for(auto &item:dead)
        {
            mWorld.DestroyBody(item);
        }

        
        
        // swap buffers
        SDL_GL_SwapWindow(mWindow);

        // Throttle to cap at 120Hz. This adaptive using a sleep adjustment. This could be improved by
		// using mm_pause or equivalent for the last millisecond.
		std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now();

		std::chrono::duration<double> target(1.0 / 120.0);
		std::chrono::duration<double> timeUsed = t2 - t1;
		std::chrono::duration<double> sleepTime = target - timeUsed + sleepAdjust;
		if (sleepTime > std::chrono::duration<double>(0))
		{
			std::this_thread::sleep_for(sleepTime);
		}

		std::chrono::steady_clock::time_point t3 = std::chrono::steady_clock::now();
		frameTime = t3 - t1;

		// Compute the sleep adjustment using a low pass filter
		sleepAdjust = 0.9 * sleepAdjust + 0.1 * (target - frameTime);
    }
    g_debugDraw.Destroy();
    OnCleanup();
    return 0;
}

void AppDelegate::OnInitData()
{
    
    //------------pyhsics world set up with Box2d------------------------
    glm::vec2 PLAYER_SIZE(12,2);
    glm::vec2 playerPos = glm::vec2(
                this->mWidth/10.0 / 2 , 
                PLAYER_SIZE.y/2.f+0.01f);
    float BALL_RADIUS = 1.25;
    glm::vec2 ballPos = playerPos + glm::vec2(0, BALL_RADIUS+1.11f);
    // set up borders
    {
        b2EdgeShape borderShape;        
        //set the borders fully bountable  with friction=0 and restitution=1      
        b2FixtureDef borderFixDef;
        borderFixDef.density = 0.0f;
        borderFixDef.friction = 0.f;
        borderFixDef.restitution = 1.0f;
        borderFixDef.shape=&borderShape;
        
        // Up
        borderShape.Set(b2Vec2(0.f,0.f),b2Vec2(mWidth/10.0f,0.f));
        b2BodyDef uBorderDef;
        uBorderDef.position.Set(0.01f, mHeight/10.f-0.01f);
        b2Body* uBorder = mWorld.CreateBody(&uBorderDef);
        uBorder->CreateFixture(&borderFixDef);
        // Down 
        b2BodyDef dBorderDef;
        dBorderDef.position.Set(0.01f, 0.01f);
        b2Body* dBorder = mWorld.CreateBody(&dBorderDef);
        dBorder->CreateFixture(&borderFixDef);

        // Left
        borderShape.Set(b2Vec2(0.f,0.f),b2Vec2(0.f,mHeight/10.0f));
        b2BodyDef lBorderDef;
        lBorderDef.position.Set(0.01f, 0.01f);
        b2Body* lBorder = mWorld.CreateBody(&lBorderDef);
        lBorder->CreateFixture(&borderFixDef);
        // Right
        b2BodyDef rBorderDef;
        rBorderDef.position.Set(mWidth/10.0f-0.01f, 0.01f);
        b2Body* rBorder = mWorld.CreateBody(&rBorderDef);
        rBorder->CreateFixture(&borderFixDef);
    }
    
    // -----------------real world----------------------------------------------
    // load shaders
    ResourceManager::LoadShader("resources/shaders/sprite.vs", "resources/shaders/sprite.fs", nullptr, "sprite");
    ResourceManager::LoadShader("resources/shaders/particle.vs", "resources/shaders/particle.fs", nullptr, "particle");
    // while(true);
    // config shaders
    glm::mat4 projection = glm::ortho(0.0f,this->mWidth/10.0f, 
         0.0f, this->mHeight/10.0f,1.0f, -1.0f);
    ShaderProgram shader = ResourceManager::GetShader("sprite");
    shader.useProgram();
    shader.setMat4("projection", projection);
    shader = ResourceManager::GetShader("particle");
    shader.useProgram();
    shader.setMat4("projection", projection);

    // set renderer
    Renderer = new SpriteRenderer(ResourceManager::GetShader("sprite"));

    // load textures
    ResourceManager::LoadTexture("resources/imgs/background.jpg", GL_FALSE, "background");
    ResourceManager::LoadTexture("resources/imgs/awesomeface.png", GL_TRUE, "face");
    ResourceManager::LoadTexture("resources/imgs/particle.png", GL_TRUE, "particle"); 
    ResourceManager::LoadTexture("resources/imgs/block.png", GL_FALSE, "block");
    ResourceManager::LoadTexture("resources/imgs/block_solid.png", GL_FALSE, "block_solid");
    ResourceManager::LoadTexture("resources/imgs/paddle.png", true, "paddle");
     // load gamelevels here 
    //  TODO: more levels will be implented in the future
     GameLevel *one = new GameLevel(); 
     one->Load("resources/jsons/one.lvl", &mWorld, this->mWidth/10, this->mHeight /20);
    //  GameLevel two; 
    //  two.Load("resources/jsons/two.lvl", &mWorld, this->mWidth/10, this->mHeight /20);
    //  GameLevel three; 
    //  three.Load("resources/jsons/three.lvl", &mWorld, this->mWidth/10, this->mHeight /20);
    //  GameLevel four; 
    //  four.Load("resources/jsons/four.lvl", &mWorld, this->mWidth/10, this->mHeight /20);
     this->Levels.push_back(one);
    //  this->Levels.push_back(two);
    //  this->Levels.push_back(three);
    //  this->Levels.push_back(four);
     this->Level = 0;


	this->player = new Player(&mWorld, playerPos, PLAYER_SIZE, ResourceManager::GetTexture("paddle"));
    
	Ball = new BallObject(&mWorld, ballPos, BALL_RADIUS, ResourceManager::GetTexture("face"));
    Particles = new ParticleGenerator(
        ResourceManager::GetShader("particle"), 
        ResourceManager::GetTexture("particle"), 
        500
    );

}

// render and swap buffers
void AppDelegate::OnRender()
{
    
    // render
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    
    glClear(GL_COLOR_BUFFER_BIT);

    // shader progrom here
    // render background
    Renderer->DrawSprite(ResourceManager::GetTexture("background"), 
        glm::vec2(mWidth/20.f, mHeight/20.f), glm::vec2(this->mWidth/10.f, this->mHeight/10.f), 0.0f);
    // render bricks
    this->Levels[this->Level]->Draw(*Renderer);
    // player
    this->player->Draw(*Renderer);
    // Particles, render this first to avoid covering main object
    Particles->Draw();
    // ball
    Ball->Draw(*Renderer);
    
}

void AppDelegate::OnCleanup()
{

}

void AppDelegate::processInput(GLfloat dt)
{
    const Uint8* currentKeyStates = SDL_GetKeyboardState( NULL );
    // move player 
    if (currentKeyStates[ SDL_SCANCODE_LEFT ])
    {
        player->getPhysicBody()->SetLinearVelocity(b2Vec2(-60,0));
    }
    if (currentKeyStates[ SDL_SCANCODE_RIGHT ])
    {
		player->getPhysicBody()->SetLinearVelocity(b2Vec2(60,0));
    }
    // set free of ball when SPACE is pressed
    if (currentKeyStates[ SDL_SCANCODE_SPACE])
		Ball->getPhysicBody()->SetLinearVelocity(b2Vec2(-40,40));
}

void AppDelegate::update(GLfloat dt)
{

}  

bool isdrag=false;
void AppDelegate::OnEvent(SDL_Event *event)
{
    if(event->type == SDL_QUIT) 
    {
        mRunning = false;
    }
    if(event->type == SDL_MOUSEBUTTONDOWN)
    {
        isdrag=true;
    }
    if(event->type == SDL_MOUSEBUTTONUP)
    {
        isdrag=false;
    }
    if(event->type == SDL_MOUSEMOTION)
    {
        int x, y;
        SDL_GetMouseState( &x, &y );
        if(isdrag)
        {
            Ball->getPhysicBody()->SetTransform(b2Vec2(x/10.f,60-y/10.f),0);
        }
    }
}

